#include <UnitTest++.h>

#include <sys/stat.h>
#include <iostream>
#include <stdarg.h>
#include <sstream>
#include <fstream>
#include <map>

#include "vcc.h"
#include "compile.h"
#include "VccException.h"
#include "memory.h"
#include "testing.h"
#include "FileTooBigException.h"
#include "UnableToOpenFileException.h"
#include "ExpectedTokenException.h"
#include "libfuncs.h"
#include "UnknownIdentifierException.h"

struct StdoutCapturingFixture {
    std::streambuf* original_stdout;
    std::stringstream captured_stdout;

    StdoutCapturingFixture() {
        this->original_stdout = std::cout.rdbuf();
        std::cout.rdbuf(this->captured_stdout.rdbuf());
    }
    
    ~StdoutCapturingFixture() {
        std::cout.rdbuf(original_stdout);
    }
};

#define WANTED_OUTPUT(s) \
    do { \
        std::string::size_type loc = captured_stdout.str().find(s); \
        if (loc == std::string::npos) { \
            CHECK_EQUAL(s, captured_stdout.str().c_str()); \
        } \
    } while (0)
    
SUITE(vcc_suite) {
    TEST(globals_are_initialized) {
        CHECK_EQUAL(false, output_mode_quiet);
        CHECK_EQUAL(false, output_mode_verbose);
    }

    TEST_FIXTURE(StdoutCapturingFixture, copyright_is_displayed) {
        VERGE1::DisplayCopyright();
        WANTED_OUTPUT("vcc v.05.May.98 Copyright (C)1997 BJ Eirich");
    }

    TEST(allocate_char_array_succeeds_if_pointer_is_unset) {
        char* buffer = 0;
        AllocateCharArray(buffer, 10);
        FreeMemory(buffer);
    }
    
    TEST(allocate_char_array_fails_if_pointer_is_already_set) {
        char* buffer = (char*)1;
        CHECK_THROW(AllocateCharArray(buffer, 10), MemoryAllocationException);
    }
    
    TEST(deallocate_char_array_succeeds_if_pointer_is_allocated) {
        char* buffer = new char[10];
        FreeMemory(buffer);
        CHECK_EQUAL(0, (int)buffer);
    }
    
    TEST(deallocate_char_array_fails_if_pointer_is_not_set) {
        char* buffer = 0;
        CHECK_THROW(FreeMemory(buffer), MemoryDeallocationException);
        CHECK(!buffer);
    }
    
    TEST(emit_c_moves_pointer_forward_one_byte) {
        char foo[] = {1,2};
        cpos = foo;
        EmitC(0);
        CHECK_EQUAL(foo + 1, cpos);
    }
    
    TEST(emit_c_writes_correct_byte) {
        char foo[] = {1,2};
        cpos = foo;
        EmitC('z');
        CHECK_EQUAL('z', foo[0]);
    }
    
    TEST(emit_w_moves_pointer_forward_two_bytes) {
        char foo[] = {1,2,3};
        cpos = foo;
        EmitW(0);
        CHECK_EQUAL(foo + 2, cpos);
    }
    
    TEST(emit_w_writes_correct_two_bytes) {
        char foo[] = {1,2,3};
        cpos = foo;
        EmitW(0xBEEF);
        unsigned short a = 0xBEEF;
        unsigned short b = *(unsigned short*)foo;
        CHECK_EQUAL(a, b);
    }
    
    TEST(emit_d_moves_pointer_forward_four_bytes) {
        char foo[] = {1,2,3,4,5};
        cpos = foo;
        EmitD(0);
        CHECK_EQUAL(foo + 4, cpos);
    }
    
    TEST(emit_d_writes_correct_four_bytes) {
        char foo[] = {1,2,3,4,5};
        cpos = foo;
        EmitD(0xDEADBEEF);
        CHECK_EQUAL(0xDEADBEEF, *(unsigned int*)foo);
    }
    
    TEST(emit_string_moves_pointer_forward_by_length_of_string_plus_one_bytes) {
        char foo[] = {1,2,3,4};
        cpos = foo;
        EmitString("vec");
        CHECK_EQUAL(foo + 4, cpos);
    }
    
    TEST(emit_string_moves_pointer_forward_one_byte_for_empty_string) {
        char foo[] = {1,2};
        cpos = foo;
        EmitString("");
        CHECK_EQUAL(foo + 1, cpos);
    }
    
    TEST(emit_string_sets_last_byte_to_zero) {
        char foo[] = {1,2};
        cpos = foo;
        EmitString("a");
        CHECK_EQUAL('\0', foo[1]);
    }
    
    TEST(handle_operand_on_unknown_identifier_fails) {
        char foo[16];
        strcpy(foo, "yourmom");
        src = foo;
        try {
            HandleOperand();
            CHECK(false);
        } catch (...) {
            std::cout << "Threw an exception, as expected." << std::endl;
        }
    }

    TEST(load_source_file_fails_on_non_existent_file) {
        std::string filename = "../samples/vc/idonotexist.txt";
        try {
            LoadTextFile(filename);
            CHECK(false);
        } catch (UnableToOpenFileException exc) {
            CHECK_EQUAL(filename, exc.get_filename());
        }
    }
    
    TEST(load_source_file_fails_if_file_size_exceeds_max_buffer_size) {
        std::string filename = "../samples/vc/bumville.vc";
        int max_size_in_bytes = 100;
        try {
            LoadTextFile(filename, max_size_in_bytes);
            CHECK(false);
        } catch (FileTooBigException exc) {
            CHECK_EQUAL("../samples/vc/bumville.vc", exc.get_filename());
            CHECK_EQUAL(max_size_in_bytes, exc.get_max_size_in_bytes());
            CHECK_EQUAL(29170, exc.get_size_in_bytes());
        }
    }
    
    TEST(load_source_file_allocates_source_buffer) {
        char* data = LoadTextFile("../samples/vc/bumville.vc");
        _Test_SetSource(data);
        CHECK(source);
        _Test_FreeSource();
    }
    
    TEST(load_source_file_loads_correct_data) {
        char* data = LoadTextFile("../samples/vc/bumville.vc");
        _Test_SetSource(data);
        std::string source_string = std::string(source);
        CHECK_EQUAL("event ", source_string.substr(0, 6));
        CHECK_EQUAL("event{}", source_string.substr(29170 - 7));
        _Test_FreeSource();
    }
    
    TEST(at_end_of_scripts) {
        _Test_SetSource("\0");
        CHECK(AtEndOfScripts());
        _Test_ClearSourceWithoutFreeing();
    }

    TEST(not_at_end_of_scripts) {
        char* data = LoadTextFile("../samples/vc/bumville.vc");
        _Test_SetSource(data);
        CHECK(!AtEndOfScripts());
        _Test_FreeSource();
    }
    
    TEST(at_true_whitespace) {
        char buffer[256];
        for (int i = 0; i < 256; i++) {
            buffer[i] = (char)i;
        }
        
        _Test_SetSource(buffer);
        
        // null character is not true whitespace
        CHECK(!AtTrueWhitespace());
        src++;
        
        // all characters 1..32 are true whitespace
        for (int i = 1; i <= (int)' '; i++) {
            CHECK(AtTrueWhitespace());
            src++;
        }
        
        // all characters 33..255 are not true whitespace
        for (int i = ((int)' ') + 1; i < 256; i++) {
            CHECK(!AtTrueWhitespace());
            src++;
        }
        
        _Test_ClearSourceWithoutFreeing();
    }
    
    TEST(get_char_type) {
        SetupCharTable();
        
        _Test_SetSource(" ");
        CHECK_EQUAL(GetCharType(), 0);
        _Test_ClearSourceWithoutFreeing();
        
        _Test_SetSource("\r");
        CHECK_EQUAL(GetCharType(), 0);
        _Test_ClearSourceWithoutFreeing();
        
        _Test_SetSource("\n");
        CHECK_EQUAL(GetCharType(), 0);
        _Test_ClearSourceWithoutFreeing();
        
        _Test_SetSource("\t");
        CHECK_EQUAL(GetCharType(), SPECIAL);
        _Test_ClearSourceWithoutFreeing();
        
        _Test_SetSource("e");
        CHECK_EQUAL(GetCharType(), IDENTIFIER);
        _Test_ClearSourceWithoutFreeing();
        
        _Test_SetSource("_");
        CHECK_EQUAL(GetCharType(), IDENTIFIER);
        _Test_ClearSourceWithoutFreeing();
        
        _Test_SetSource("{");
        CHECK_EQUAL(GetCharType(), SPECIAL);
        _Test_ClearSourceWithoutFreeing();
        
        _Test_SetSource(".");
        CHECK_EQUAL(GetCharType(), IDENTIFIER);
        _Test_ClearSourceWithoutFreeing();
        
        _Test_SetSource("0");
        CHECK_EQUAL(GetCharType(), DIGIT);
        _Test_ClearSourceWithoutFreeing();
    }
    
    TEST(can_recognize_start_of_c_comment) {
        SetupCharTable();
        
        _Test_SetSource("/*");
        CHECK(AtMultiLineComment());
        _Test_ClearSourceWithoutFreeing();
    }
    
    TEST(can_recognize_end_of_c_comment) {
        SetupCharTable();
        
        _Test_SetSource("*/");
        CHECK(AtEndOfMultiLineComment());
        _Test_ClearSourceWithoutFreeing();
    }
    
    TEST(can_parse_cpp_comment) {
        SetupCharTable();
        
        _Test_SetSource("// This is a dramatization.\nx");
        ParseWhitespace();
        CHECK_EQUAL('x', *src);
        _Test_ClearSourceWithoutFreeing();
    }
    
    TEST(can_parse_cpp_comment_at_end_of_file) {
        SetupCharTable();
        
        _Test_SetSource("// Be careful omg.");
        ParseWhitespace();
        CHECK(AtEndOfScripts());
        _Test_ClearSourceWithoutFreeing();
    }
    
    TEST(can_parse_smallest_possible_cpp_comment_at_end_of_file) {
        SetupCharTable();
        
        _Test_SetSource("//");
        ParseWhitespace();
        CHECK(AtEndOfScripts());
        _Test_ClearSourceWithoutFreeing();
    }
    
    TEST(can_parse_c_comment) {
        SetupCharTable();
        
        _Test_SetSource("/* This is a dramatization. */x");
        ParseWhitespace();
        CHECK_EQUAL('x', *src);
        _Test_ClearSourceWithoutFreeing();
    }
    
    TEST(can_parse_c_comment_at_end_of_file) {
        SetupCharTable();
        
        _Test_SetSource("/* Be careful omg. */");
        ParseWhitespace();
        CHECK(AtEndOfScripts());
        _Test_ClearSourceWithoutFreeing();
    }
    
    TEST(can_parse_smallest_possible_c_comment) {
        SetupCharTable();
        
        _Test_SetSource("/**/x");
        SkipToEndOfMultiLineComment();
        CHECK_EQUAL('x', *src);
        _Test_ClearSourceWithoutFreeing();
    }
    
    TEST(fail_on_nested_c_comment) {
        SetupCharTable();
        
        _Test_SetSource("/*/*");
        ParseWhitespace();
        CHECK(IsErrorDetected());
        _Test_ClearSourceWithoutFreeing();
    }
    
    TEST(fail_on_unclosed_c_comment) {
        SetupCharTable();
        
        _Test_SetSource("/*");
        ParseWhitespace();
        CHECK(IsErrorDetected());
        CHECK_EQUAL("Unclosed multi-line comment, starting on line 1 (1)", GetErrorMessage());
        _Test_ClearSourceWithoutFreeing();
    }
    
    TEST(can_parse_all_whitespace_types) {
        SetupCharTable();
        
        _Test_SetSource(" \t\r\n/**///\31\1");
        ParseWhitespace();
        CHECK_EQUAL('\0', *src);
        _Test_ClearSourceWithoutFreeing();
    }
    
    TEST(get_token) {
        SetupCharTable();
        
        _Test_SetSource("     \n/**///\n\tevent{}");
        
        CHECK(GetToken());
        CHECK_EQUAL("EVENT", token);
        
        CHECK(GetToken());
        CHECK_EQUAL("{", token);
        
        CHECK(GetToken());
        CHECK_EQUAL("}", token);
        
        CHECK(!GetToken());
        
        _Test_ClearSourceWithoutFreeing();
    }
    
    TEST(parse_whitespace_has_no_effect) {
        _Test_SetSource("event");
        InitCompileSystem();
        
        ParseWhitespace();
        CHECK_EQUAL(source, src);
        
        FreeMemory(code);
        _Test_ClearSourceWithoutFreeing();
        std::cout << "source=" << (int)source << std::endl;
    }
    
    TEST(parse_event_throws_unknown_identifier_exception) {
        _Test_SetSource("event{}foo");
        InitCompileSystem();

        CHECK(ParseEvent());
        try {
            ParseEvent();
            CHECK(false);
        } catch (UnknownIdentifierException exc) {
            CHECK_EQUAL("FOO", exc.get_identifier());
            CHECK_EQUAL(1, exc.get_line());
        }
        
        FreeMemory(code);
        _Test_ClearSourceWithoutFreeing();
    }
    
    TEST(parse_event_throws_expected_event_token_exception) {
        _Test_SetSource("event{}if");
        InitCompileSystem();

        CHECK(ParseEvent());
        try {
            ParseEvent();
            CHECK(false);
        } catch (ExpectedTokenException exc) {
            CHECK_EQUAL("EVENT", exc.get_token_expected());
            CHECK_EQUAL("IF", exc.get_token_received());
            CHECK_EQUAL(1, exc.get_line());
        } catch (VccException& exc) {
            std::cout << exc.get_message() << std::endl;
        }
        
        FreeMemory(code);
        _Test_ClearSourceWithoutFreeing();
    }
    
    TEST(parse_event) {
        char* data = LoadTextFile("../samples/vc/bumville.vc");
        _Test_SetSource(data);
        InitCompileSystem();
        
        int num_parsed_events = 0;
        try {
            while (ParseEvent()) {
                num_parsed_events++;
            }
        } catch (VccException& exc) {
            CHECK(false);
            std::cout << exc.get_message() << std::endl;
        }
        
        CHECK_EQUAL(66, num_parsed_events);

        _Test_FreeSource();
        FreeMemory(code);
    }

    TEST(parse_simple_event_fails_on_unrecognized_identifier_as_argument) {
        _Test_SetSource("event {\n"
                        "  warp(yourmom,151,0);\n"
                        "}");
        InitCompileSystem();
        
        try {
            ParseEvent();
            CHECK(false);
        } catch (VccException exc) {
            std::cout << exc.get_message() << std::endl;
        }
        
        _Test_ClearSourceWithoutFreeing();
        FreeMemory(code);
    }

    TEST(parse_simple_event) {
        _Test_SetSource("event {\n"
                        "key[0]=1;\n"
                        "}");
        InitCompileSystem();
        
        try {
            bool result = ParseEvent();
            CHECK(result);
        } catch (VccException& exc) {
            std::cout << exc.get_message() << std::endl;
        }
        
        std::cout << (int)((int)cpos - (int)code) << " bytes emitted" << std::endl;
    
        _Test_ClearSourceWithoutFreeing();
        FreeMemory(code);
    }
}
